<?php

declare(strict_types=1);

/*
 * This file is part of PHP CS Fixer.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PhpCsFixer\Tests\Fixer\Phpdoc;

use PhpCsFixer\Tests\Test\AbstractFixerTestCase;
use PhpCsFixer\WhitespacesFixerConfig;

/**
 * @internal
 *
 * @covers \PhpCsFixer\Fixer\Phpdoc\PhpdocTagNoNamedArgumentsFixer
 *
 * @extends AbstractFixerTestCase<\PhpCsFixer\Fixer\Phpdoc\PhpdocTagNoNamedArgumentsFixer>
 *
 * @phpstan-import-type _AutogeneratedInputConfiguration from \PhpCsFixer\Fixer\Phpdoc\PhpdocTagNoNamedArgumentsFixer
 *
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
 */
final class PhpdocTagNoNamedArgumentsFixerTest extends AbstractFixerTestCase
{
    /**
     * @param _AutogeneratedInputConfiguration $configuration
     *
     * @dataProvider provideFixCases
     */
    public function testFix(string $expected, ?string $input = null, array $configuration = [], ?WhitespacesFixerConfig $whitespacesFixerConfig = null): void
    {
        $this->fixer->configure($configuration);
        if (null !== $whitespacesFixerConfig) {
            $this->fixer->setWhitespacesConfig($whitespacesFixerConfig);
        }
        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<string, array{string, 1?: null|string, 2?: _AutogeneratedInputConfiguration}>
     */
    public static function provideFixCases(): iterable
    {
        yield 'do not add for anonymous class' => [
            <<<'PHP'
                <?php
                new class () {};
                PHP,
        ];

        yield 'do not add for internal class' => [
            <<<'PHP'
                <?php
                /**
                 * @internal
                 */
                class Foo {}
                PHP,
            null,
            ['fix_internal' => false],
        ];

        yield 'add for internal class by default' => [
            <<<'PHP'
                <?php
                /**
                 * @internal
                 * @no-named-arguments
                 */
                class Foo {}
                PHP,
            <<<'PHP'
                <?php
                /**
                 * @internal
                 */
                class Foo {}
                PHP,
            ['fix_internal' => true],
        ];

        yield 'create PHPDoc comment' => [
            <<<'PHP'
                <?php

                /**
                 * @no-named-arguments
                 */
                class Foo {}
                PHP,
            <<<'PHP'
                <?php
                class Foo {}
                PHP,
        ];

        yield 'create PHPDoc with description' => [
            <<<'PHP'
                <?php

                /**
                 * @no-named-arguments The description
                 */
                class Foo {}
                PHP,
            <<<'PHP'
                <?php
                class Foo {}
                PHP,
            ['description' => 'The description'],
        ];

        yield 'add description' => [
            <<<'PHP'
                <?php
                /**
                 * @no-named-arguments New description
                 */
                class Foo {}
                PHP,
            <<<'PHP'
                <?php
                /**
                 * @no-named-arguments
                 */
                class Foo {}
                PHP,
            ['description' => 'New description'],
        ];

        yield 'change description' => [
            <<<'PHP'
                <?php
                /**
                 * @no-named-arguments New description
                 */
                class Foo {}
                PHP,
            <<<'PHP'
                <?php
                /**
                 * @no-named-arguments Old description
                 */
                class Foo {}
                PHP,
            ['description' => 'New description'],
        ];

        yield 'remove description' => [
            <<<'PHP'
                <?php
                /**
                 * @no-named-arguments
                 */
                class Foo {}
                PHP,
            <<<'PHP'
                <?php
                /**
                 * @no-named-arguments Description to remove
                 */
                class Foo {}
                PHP,
        ];

        yield 'multiple classes' => [
            <<<'PHP'
                <?php

                /**
                 * @no-named-arguments
                 */
                class Foo {}

                new class {};

                /**
                 * @no-named-arguments
                 */
                class Bar {}
                PHP,
            <<<'PHP'
                <?php
                class Foo {}

                new class {};

                class Bar {}
                PHP,
        ];

        yield 'tabs and Windows Line endings' => [
            str_replace(
                ['    ', "\n"],
                ["\t", "\r\n"],
                <<<'PHP'
                    <?php

                    /**
                     * @no-named-arguments
                     */
                    class Foo {}
                    PHP,
            ),
            str_replace(
                ['    ', "\n"],
                ["\t", "\r\n"],
                <<<'PHP'
                    <?php

                    class Foo {}
                    PHP,
            ),
            [],
            new WhitespacesFixerConfig("\t", "\r\n"),
        ];
    }

    /**
     * @param _AutogeneratedInputConfiguration $configuration
     *
     * @dataProvider provideFix80Cases
     *
     * @requires PHP >= 8.0
     */
    public function testFix80(string $expected, ?string $input = null, array $configuration = []): void
    {
        $this->testFix($expected, $input, $configuration);
    }

    /**
     * @return iterable<string, array{string, 1?: null|string, 2?: _AutogeneratedInputConfiguration}>
     */
    public static function provideFix80Cases(): iterable
    {
        yield 'do not add for attribute class' => [
            <<<'PHP'
                <?php
                #[Attribute(flags: Attribute::TARGET_METHOD)]
                final class MyAttributeClass {}
                PHP,
            null,
            ['fix_attribute' => false],
        ];

        yield 'add for attribute class' => [
            <<<'PHP'
                <?php

                /**
                 * @no-named-arguments
                 */
                #[Attribute(flags: Attribute::TARGET_METHOD)]
                final class MyAttributeClass {}
                PHP,
            <<<'PHP'
                <?php
                #[Attribute(flags: Attribute::TARGET_METHOD)]
                final class MyAttributeClass {}
                PHP,
            ['fix_attribute' => true],
        ];

        yield 'do not add for attribute class (with alias)' => [
            <<<'PHP'
                <?php
                namespace Foo;
                use Attribute as TheAttributeClass;
                #[TheAttributeClass(flags: TheAttributeClass::TARGET_METHOD)]
                final class MyAttributeClass {}
                PHP,
            null,
            ['fix_attribute' => false],
        ];
    }

    /**
     * @dataProvider provideFix81Cases
     *
     * @requires PHP >= 8.1
     */
    public function testFix81(string $expected, ?string $input = null): void
    {
        $this->testFix($expected, $input);
    }

    /**
     * @return iterable<string, array{0: string, 1?: string}>
     */
    public static function provideFix81Cases(): iterable
    {
        yield 'enum' => [
            <<<'PHP'
                <?php

                /**
                 * @no-named-arguments
                 */
                enum Size {
                    case Small;
                    case Medium;
                    case Large;

                    public static function fromLength(int $cm) {
                        return match(true) {
                            $cm < 50 => static::Small,
                            $cm < 100 => static::Medium,
                        default => static::Large,
                    };
                  }
                }
                PHP,
            <<<'PHP'
                <?php
                enum Size {
                    case Small;
                    case Medium;
                    case Large;

                    public static function fromLength(int $cm) {
                        return match(true) {
                            $cm < 50 => static::Small,
                            $cm < 100 => static::Medium,
                        default => static::Large,
                    };
                  }
                }
                PHP,
        ];
    }

    /**
     * @param _AutogeneratedInputConfiguration $configuration
     *
     * @dataProvider provideFix82Cases
     *
     * @requires PHP >= 8.2
     */
    public function testFix82(string $expected, ?string $input = null, array $configuration = []): void
    {
        $this->testFix($expected, $input, $configuration);
    }

    /**
     * @return iterable<string, array{string, 1?: null|string, 2?: _AutogeneratedInputConfiguration}>
     */
    public static function provideFix82Cases(): iterable
    {
        yield 'do not add for attribute (readonly) class' => [
            <<<'PHP'
                <?php

                /**
                 * @no-named-arguments
                 */
                #[FooAttribute]
                final readonly class NotAttributeClass1 {}

                #[Attribute(flags: Attribute::TARGET_METHOD)]
                final readonly class MyAttributeClass {}

                /**
                 * @no-named-arguments
                 */
                final readonly class NoAttributes {}

                /**
                 * @no-named-arguments
                 */
                #[FooAttribute]
                #[BarAttribute]
                #[BazAttribute]
                final readonly class NotAttributeClass2 {}
                PHP,
            <<<'PHP'
                <?php
                #[FooAttribute]
                final readonly class NotAttributeClass1 {}

                #[Attribute(flags: Attribute::TARGET_METHOD)]
                final readonly class MyAttributeClass {}

                final readonly class NoAttributes {}

                #[FooAttribute]
                #[BarAttribute]
                #[BazAttribute]
                final readonly class NotAttributeClass2 {}
                PHP,
            ['fix_attribute' => false],
        ];
    }

    /**
     * @dataProvider provideFixPre85Cases
     *
     * @requires PHP ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0
     */
    public function testFixPre85(string $expected, ?string $input = null): void
    {
        $this->doTest($expected, $input);
    }

    /**
     * @return iterable<string, array{0: string, 1?: string}>
     */
    public static function provideFixPre85Cases(): iterable
    {
        // the below case is fatal error in PHP 8.5+ (https://github.com/php/php-src/pull/19154)
        yield 'always add for abstract attribute class' => [
            <<<'PHP'
                <?php
                namespace Foo;
                /**
                 * @no-named-arguments
                 */
                #[\Attribute(flags: \Attribute::TARGET_METHOD)]
                abstract class MyAttributeClass {}
                PHP,
            <<<'PHP'
                <?php
                namespace Foo;
                #[\Attribute(flags: \Attribute::TARGET_METHOD)]
                abstract class MyAttributeClass {}
                PHP,
        ];
    }
}
